package com.scpii.api.common.auth.token;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.sql.DataSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import com.scpii.api.common.auth.ClientAuthentication;
import com.scpii.api.common.auth.SerializationUtils;

public class JdbcTokenStore implements TokenStore {
	private static final Log LOG = LogFactory.getLog(JdbcTokenStore.class);

	private static final String DEFAULT_ACCESS_TOKEN_AUTHENTICATION_SELECT_STATEMENT = "select TOKEN_ID, AUTHENTICATION_ID, DEVICE_ID, APP_ID, "
			+ "REFRESH_TOKEN,TOKEN, AUTHENTICATION, CREATE_DT,UPDATE_DT,CREATE_BY, UPDATE_BY from access_token where TOKEN_ID=?";

	private static final String DEFAULT_ACCESS_TOKEN_DELETE_STATEMENT = "delete from access_token where TOKEN_ID = ?";

	private static final String DEFAULT_ACCESS_TOKEN_SELECT_STATEMENT = "select TOKEN_ID, TOKEN from access_token where TOKEN_ID = ?";

	private String selectAccessTokenAuthenticationSql = DEFAULT_ACCESS_TOKEN_AUTHENTICATION_SELECT_STATEMENT;

	private String deleteAccessTokenSql = DEFAULT_ACCESS_TOKEN_DELETE_STATEMENT;

	private String selectAccessTokenSql = DEFAULT_ACCESS_TOKEN_SELECT_STATEMENT;

	private JdbcTemplate jdbcTemplate;

	public JdbcTokenStore(DataSource dataSource) {
		super();
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	}

	@Override
	public ClientAuthentication readAuthentication(String token) {
		ClientAuthentication authentication = null;
		try {
			authentication = jdbcTemplate.queryForObject(
					selectAccessTokenAuthenticationSql,
					new RowMapper<ClientAuthentication>() {
						public ClientAuthentication mapRow(ResultSet rs,
								int rowNum) throws SQLException {
							return deserializeAuthentication(rs.getBytes(7));
						}
					}, extractTokenKey(token));
		} catch (EmptyResultDataAccessException e) {
			if (LOG.isInfoEnabled()) {
				LOG.info("Failed to find access token for token " + token);
			}
		} catch (IllegalArgumentException e) {
			LOG.warn("Failed to deserialize authentication for " + token);
			removeAccessToken(token);
		}

		return authentication;
	}

	public void removeAccessToken(String tokenValue) {
		jdbcTemplate.update(deleteAccessTokenSql, extractTokenKey(tokenValue));
	}

	protected String extractTokenKey(String value) {
		if (value == null) {
			return null;
		}
		MessageDigest digest;
		try {
			digest = MessageDigest.getInstance("MD5");
		} catch (NoSuchAlgorithmException e) {
			throw new IllegalStateException(
					"MD5 algorithm not available.  Fatal (should be in the JDK).");
		}

		try {
			byte[] bytes = digest.digest(value.getBytes("UTF-8"));
			return String.format("%032x", new BigInteger(1, bytes));
		} catch (UnsupportedEncodingException e) {
			throw new IllegalStateException(
					"UTF-8 encoding not available.  Fatal (should be in the JDK).");
		}
	}

	public void setSelectAccessTokenAuthenticationSql(
			String selectAccessTokenAuthenticationSql) {
		this.selectAccessTokenAuthenticationSql = selectAccessTokenAuthenticationSql;
	}

	public void setDeleteAccessTokenSql(String deleteAccessTokenSql) {
		this.deleteAccessTokenSql = deleteAccessTokenSql;
	}

	public void setSelectAccessTokenSql(String selectAccessTokenSql) {
		this.selectAccessTokenSql = selectAccessTokenSql;
	}

	@Override
	public AuthAccessToken readAccessToken(String accessTokenValue) {
		AuthAccessToken accessToken = null;

		try {
			accessToken = jdbcTemplate.queryForObject(selectAccessTokenSql,
					new RowMapper<AuthAccessToken>() {
						public AuthAccessToken mapRow(ResultSet rs, int rowNum)
								throws SQLException {
							return deserializeAccessToken(rs.getBytes(2));
						}
					}, extractTokenKey(accessTokenValue));
		} catch (EmptyResultDataAccessException e) {
			if (LOG.isInfoEnabled()) {
				LOG.info("Failed to find access token for token "
						+ accessTokenValue);
			}
		} catch (IllegalArgumentException e) {
			LOG.warn("Failed to deserialize access token for "
					+ accessTokenValue);
			removeAccessToken(accessTokenValue);
		}

		return accessToken;
	}

	protected ClientAuthentication deserializeAuthentication(
			byte[] authentication) {
		return SerializationUtils.deserialize(authentication);
	}

	protected AuthAccessToken deserializeAccessToken(byte[] token) {
		return SerializationUtils.deserialize(token);
	}
}
